﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using Tao.OpenGl;
using Tao.Platform.Windows;

using CutAways.Geometry;
using CutAways.Modeling;
using CutAways.Shaders;
using CutAways.Illustration;
using System.IO;

namespace CutAways
{
	partial class MyGLControl : SimpleOpenGlControl
	{
		private Camera myCamera = new Camera();

		private Camera currentCutViewpoint;
		private Camera futureCutViewpoint;
		private Animation animation;
		private int numberOfObjectsWithCoumputedCuttingParams;

		private Timer timer;
        private Model model;

		private GeometricObject workObject;
		
        private BoxCutShader boxCutShader;
        private TubeCutShader tubeCutShader;
        //private ShaderHolder defaultShader;

        private Point prevMouseLocation;
        private bool useShaders;

		private ProgramMode programMode;
		
		// constants
		public static readonly float LEVEL_INCREMENT_OF_CUT = 0.1f;
		public static readonly int ANIMATION_OPENING_CLOSING_FRAME_COUNT = 10;
		public static readonly int ANIMATION_CAMERA_MOVING_FRAMES_PER_ONE_LENGTH_UNIT_COUNT = 15;

		public Model Model
		{
			get { return model; }
			set { model = value; }
		}

		public Camera Camera
		{
			get { return myCamera; }
		}

		public void SetCamera(Camera cam)
		{
			this.myCamera = cam;
		}

		public ProgramMode ProgramMode
		{
			get { return programMode; }
			set { programMode = value; }
		}

		public GeometricObject WorkObject
		{
			get { return workObject; }
			set
			{
				workObject = value;
			}
		}
		
		public MyGLControl()
		{
			myCamera = new Camera();
            InitializeComponent();
			
			model = null;
			programMode = ProgramMode.Start;
            //
            prevMouseLocation = new Point(-1, -1);
            useShaders = false;
			workObject = null;
			animation = null;
			currentCutViewpoint = futureCutViewpoint = null;
			numberOfObjectsWithCoumputedCuttingParams = 0;
		}

		void timer_Tick(object sender, EventArgs e)
		{
			Invalidate();
		}

		public void Initialize() // musi se zavolat pred pouzitim
		{
			InitializeContexts();
			glInit();

			Paint += new PaintEventHandler(glPaint);
			Resize += new EventHandler(glResize);
			MouseDown += new MouseEventHandler(mouseDown);
            MouseWheel += new MouseEventHandler(mouseWheel);
            MouseMove += new MouseEventHandler(mouseMove);
			KeyDown += new KeyEventHandler(keyDown);

			timer = new Timer();
			timer.Interval = 30;
			timer.Tick += new EventHandler(timer_Tick);
			timer.Start();

			glResize(null, null);

			//string start = Application.StartupPath;
			//string[] vsFile = { Path.Combine(start, "Shaders/perPixel.vert") };
			//string[] fsFile = { Path.Combine(start, "Shaders/directionalLightShader.frag"),
			//                    Path.Combine(start, "Shaders/pointLightShader.frag"),
			//                    Path.Combine(start, "Shaders/spotLightShader.frag"),
			//                    Path.Combine(start, "Shaders/textureShader.frag"),
			//                    Path.Combine(start, "Shaders/perPixel.frag") };

			loadShaders();
            //shaderHolder.LoadShaders();
		}

		private void loadShaders()
		{
			string start = Application.StartupPath;

			string[] vsFile = { Path.Combine(start, "Shaders/perPixelBox.vert") };
			string[] fsFile = { Path.Combine(start, "Shaders/directionalLightShader.frag"),
                                Path.Combine(start, "Shaders/pointLightShader.frag"),
                                Path.Combine(start, "Shaders/spotLightShader.frag"),
                                Path.Combine(start, "Shaders/textureShader.frag"),
                                Path.Combine(start, "Shaders/perPixelBoxCut.frag") };

			boxCutShader = new BoxCutShader(vsFile, fsFile, "perPixelBoxCut");

			string[] vsFile2 = { Path.Combine(start, "Shaders/perPixelTube.vert") };
			string[] fsFile2 = {Path.Combine(start, "Shaders/directionalLightShader.frag"),
                                Path.Combine(start, "Shaders/pointLightShader.frag"),
                                Path.Combine(start, "Shaders/spotLightShader.frag"),
                                Path.Combine(start, "Shaders/textureShader.frag"),
                                Path.Combine(start, "Shaders/perPixelTubeCut.frag") };

			tubeCutShader = new TubeCutShader(vsFile2, fsFile2, "perPixelTubeCut");
		}

		public void ResetView()
		{
			myCamera.Reset();
			if (programMode == ProgramMode.Illustrator)
				myCamera.Eye = Vector.SetLength(myCamera.Eye, 2f);
			else if (programMode == ProgramMode.EditBoundingBox && workObject != null)
				myCamera.Eye = Vector.SetLength(myCamera.Eye, workObject.AxesBoundingBox.Radius * 2f);	
		}

		void keyDown(object sender, KeyEventArgs e)
		{
            switch (e.KeyCode)
            { 
                //case Keys.S: /**/ // to delete !!!
                //    ShadersSwitch(!this.useShaders);
                //    break;
				case Keys.R:
					ResetView();
					if(workObject != null)
						workObject.AxesBoundingBox.RotationAngles = new Vector();
					break;
				case Keys.X:
					ResetView();
					myCamera.Eye = Vector.SetLength(new Vector(-1f, 0, 0), myCamera.Eye.GetLength());
					break;
				case Keys.Y:
					ResetView();
					myCamera.Eye = Vector.SetLength(new Vector(0, -1f, 0), myCamera.Eye.GetLength());
					myCamera.Up = new Vector(0, 0, 1f);
					break;
				case Keys.Z:
					ResetView();
					myCamera.Eye = Vector.SetLength(new Vector(0, 0, 1f), myCamera.Eye.GetLength());
					break;
				case Keys.Oemcomma:
					if (workObject != null)
						workObject.AxesBoundingBox.RotationAngles.Z += 1;
					break;
				case Keys.OemPeriod:
					if (workObject != null)
						workObject.AxesBoundingBox.RotationAngles.Z -= 1;
					break;
				case Keys.B:
					if (model != null)
						model.DrawBoundingBoxes = !model.DrawBoundingBoxes;
					break;
				case Keys.M:
					if (model != null)
						model.DrawMaterials = !model.DrawMaterials;
					break;
				//case Keys.Space: /**/ // to delete !!!
				//    if (model != null && model.SelectedObject != null)
				//    {
				//        foreach (GeometricObject o in model.Objects)
				//            if (o.Visible && o != model.SelectedObject)
				//                o.ComputeCuttingParametres(model.SelectedObject, myCamera);
				//    }
				//	break;
            }
		}

		public void ShadersSwitch(bool state)
		{
			useShaders = state;
			if (useShaders)
				Gl.glUseProgram(boxCutShader.Program);
			else
				Gl.glUseProgram(0);
		}

		void mouseDown(object sender, MouseEventArgs e)
		{
			prevMouseLocation = e.Location;
			if (programMode == ProgramMode.Illustrator && e.Button == MouseButtons.Left)
				if (model != null)
					model.SelectObjectOnPosition(myCamera, e.X, e.Y);
		}

        void mouseWheel(object sender, MouseEventArgs e)
        {
            if (e.Delta > 0)
                myCamera.moveForward(30);
            else
                myCamera.moveBackward(30);
        }

        void mouseMove(object sender, MouseEventArgs e)
        {
			int dX = e.X - prevMouseLocation.X;
			int dY = e.Y - prevMouseLocation.Y;

			if (programMode == ProgramMode.EditBoundingBox && e.Button == MouseButtons.Left)
			{
				rotateBoundingBox(dX / 2f, dY / 2f);
			}
			else if (e.Button == MouseButtons.Right)
			{				
				if (dX > 0)
					myCamera.strafeRight(30, dX);
				else if (dX < 0)
					myCamera.strafeLeft(30, -dX);
				if (dY > 0)
					myCamera.strafeDown(30, dY);
				else if (dY < 0)
					myCamera.strafeUp(30, -dY);
			}	
			prevMouseLocation = e.Location;		
        }

		private void rotateBoundingBox(float dX, float dY)
		{
			if (Math.Abs(dX) > Math.Abs(/*4 * */dY))
				workObject.AxesBoundingBox.RotationAngles.Y += dX;
			else if (Math.Abs(dY) > Math.Abs(/*4 * */dX))
				workObject.AxesBoundingBox.RotationAngles.X += dY;
		}

		private void glInit()
		{
			Gl.glShadeModel(Gl.GL_SMOOTH);								// enable smooth shading
			Gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);					// white background
			Gl.glClearDepth(1.0f);										// depth buffer setup
			Gl.glEnable(Gl.GL_DEPTH_TEST);								// enables depth testing
			Gl.glDepthFunc(Gl.GL_LEQUAL);								// type of depth test
			Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);	// nice perspective calculations
            
			//Gl.glDisable(Gl.GL_CULL_FACE); // :-!
            
			Gl.glLightModeli(Gl.GL_LIGHT_MODEL_TWO_SIDE, Gl.GL_FALSE);

            //// default materials
            //float[] ambientMat = { 0.4f, 0.4f, 0.4f, 1 };
            //Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT, ambientMat);
            //float[] diffuseMat = { 0.6f, 0.6f, 0.6f, 1 };
            //Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_DIFFUSE, diffuseMat);
            //float[] specularMat = { 1.0f, 1.0f, 1.0f, 1 };
            //Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_SPECULAR, specularMat);
            //float shininess = 64;
            //Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_SHININESS, ref shininess);

            float[] ambient2 = { 0.4f, 0.4f, 0.4f, 1 };
            float[] diffuse2 = { 1, 1, 1, 1 };
            float[] specular2 = { 1, 1, 1, 1 };
            float[] globalAmbient2 = { 0, 0, 0, 1 };

            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient2);
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse2);
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, specular2);
            Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, globalAmbient2);

            // enable lights
            Gl.glEnable(Gl.GL_LIGHT0);
            Gl.glEnable(Gl.GL_LIGHTING);
		}

		void glResize(object sender, EventArgs e)
		{
			Gl.glViewport(0, 0, Width, Height);

			Gl.glMatrixMode(Gl.GL_PROJECTION);
			Gl.glLoadIdentity();
			Glu.gluPerspective(90.0f, (float)Width / (float)Height, 0.1f, 100.0f);

			Gl.glMatrixMode(Gl.GL_MODELVIEW);
			Gl.glLoadIdentity();
		}

		void glPaint(object sender, PaintEventArgs e)
		{
			switch (programMode)
			{
				case ProgramMode.Start:
				case ProgramMode.Illustrator:
					Gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // black background
					break;
				case ProgramMode.EditBoundingBox:
					Gl.glClearColor(0.9f, 0.85f, 0.95f, 1.0f); // gray background
					break;
				case ProgramMode.CutView:
					Gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); // white background
					if (animation != null)
					{
						myCamera = animation.CurrentViewpoint;
						animation.NextFrame(); // anime
					}
					break;
			}

			Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);	// Clear the Screen and the Depth Buffer
			Gl.glLoadIdentity();					                        // reset the current modelview matrix

			Gl.glMatrixMode(Gl.GL_MODELVIEW);

			float[] lightPos = { 0, 100, 100, 1 };
			Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_POSITION, lightPos);
			float[] lightDir = { 0, -1, -1, 1 };
			Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPOT_DIRECTION, lightDir);

			Glu.gluLookAt(myCamera.Eye.X, myCamera.Eye.Y, myCamera.Eye.Z,
							myCamera.Center.X, myCamera.Center.Y, myCamera.Center.Z,
							myCamera.Up.X, myCamera.Up.Y, myCamera.Up.Z);

			//if (useShaders)
			//Gl.glUseProgram(0);

			if (this.programMode != ProgramMode.CutView && this.programMode != ProgramMode.Start) // pokud jsem v user modu tak nezobrazuj osy
			{
				Gl.glDisable(Gl.GL_LIGHTING);
				Gl.glBegin(Gl.GL_LINES); // draw axes
				{
					Gl.glColor3f(1, 0, 0);
					Gl.glVertex3f(0, 0, 0);
					Gl.glVertex3f(100, 0, 0);
					Gl.glColor3f(0, 0, 1);
					Gl.glVertex3f(0, 0, 0);
					Gl.glVertex3f(0, 100, 0);
					Gl.glColor3f(0, 1, 0);
					Gl.glVertex3f(0, 0, 0);
					Gl.glVertex3f(0, 0, 100);
				}
				Gl.glEnd();
				Gl.glEnable(Gl.GL_LIGHTING);
			}

            //if (useShaders)
            //    Gl.glUseProgram(boxCutShader.Program);

			// DRAW HERE
            switch (programMode)
            {
                case ProgramMode.Start:
                    break;
                case ProgramMode.Illustrator:
					model.Draw();
					break;
				case ProgramMode.EditBoundingBox:
					drawBoundingBoxEditing();
					break;
                case ProgramMode.CutView:
					drawModelWithCuts();
                    break;
            }
			Gl.glFlush();
		}

		private void drawModelWithCuts()
		{
			float cutOpenFactor;
			OcclusionGraph occlusionGraph;
			if (this.animation != null)
				cutOpenFactor = this.animation.CutOpenFactor;
			else
				cutOpenFactor = 1f;
			if (this.currentCutViewpoint != null)
				occlusionGraph = model.ViewpointSet.GetOcclusionGraphOf(this.currentCutViewpoint);
			else
				occlusionGraph = model.ViewpointSet.GetOcclusionGraphAt(0); /**/
			// -------------------------------------------------------------------------------------------------
			model.DrawWithCuts(model.SelectedObject, occlusionGraph, cutOpenFactor, boxCutShader, tubeCutShader);
			// -------------------------------------------------------------------------------------------------
		}

		private void drawBoundingBoxEditing()
		{
			workObject.CreateOrientedBoundingBox(model);
			
			Gl.glPushMatrix();

			Vector rotations = workObject.AxesBoundingBox.RotationAngles;
			Vector abbCenter = workObject.AxesBoundingBox.Center;

			Gl.glRotatef(rotations.Z, 0f, 0f, 1f);
			Gl.glRotatef(rotations.Y, 0f, 1f, 0f);
			Gl.glRotatef(rotations.X, 1f, 0f, 0f);
				
			Gl.glTranslatef(-abbCenter.X, -abbCenter.Y, -abbCenter.Z);

			// draw object
			model.DrawObject(workObject);	
			
			Gl.glColor3f(0.5f, 0.5f, 0.5f);
			workObject.AxesBoundingBox.Draw();
			
			if (workObject.CuttingType == CuttingType.WedgeTube)
			{
				workObject.CreateBoundingTube(model);
				Gl.glColor3f(0.9f, 0.7f, 0f);
				workObject.BoundingTube.Draw();
			}
			else
			{
				Gl.glColor3f(0f, 0.6f, 1f);
					workObject.OrientedBoundingBox.Draw();
			}
			
			Gl.glPopMatrix();
		}
		
		/// <summary>
		/// Tahle funkce se vola pri nastaveni modu na ProgramMode.CutView.
		/// Nastavi vsechny parametry a spusti animaci.
		/// </summary>
		public void Walk()
		{
			ShadersSwitch(true);
			this.currentCutViewpoint = this.futureCutViewpoint = null;
			this.animation = null;
			if (this.model != null)
				foreach (GeometricObject o in this.model.Objects)
					o.CuttingParametres = null;
		}

		/// <summary>
		/// nastav kameru na nejvhodnejsi pohled
		/// </summary>
		public void MoveToBestViewpointForSelectedObject(GeometricObject focusedObject)
		{
			if (focusedObject == null)
			{
				animation = null;
				return;
			}
			this.futureCutViewpoint = findBestViewpointFor(focusedObject);
			int startFrame;
			if (model.SelectedObject == null || (animation != null && !animation.IsFinished) || this.numberOfObjectsWithCoumputedCuttingParams == 0)
				startFrame = ANIMATION_OPENING_CLOSING_FRAME_COUNT;
			else
				startFrame = 0;
			// nastav animaci
			animation = new Animation(myCamera, this.futureCutViewpoint, startFrame, ANIMATION_OPENING_CLOSING_FRAME_COUNT, ANIMATION_CAMERA_MOVING_FRAMES_PER_ONE_LENGTH_UNIT_COUNT);
			// ... reakce na udalosti animace
			animation.Moved += delegate
			{
				model.SelectedObject = focusedObject;
				this.currentCutViewpoint = this.futureCutViewpoint;
				computeNeededCuttingParametres();
			};
			animation.Finished += delegate { this.animation = null; };
		}

		/// <summary>
		/// vypocte parametry vsech rezu
		/// </summary>
		private void computeNeededCuttingParametres()
		{
			if (model == null || model.SelectedObject == null)
				return;
			int count = 0;
			foreach (GeometricObject o in model.GetObjectsToCut(model.SelectedObject, model.ViewpointSet.GetOcclusionGraphOf(this.currentCutViewpoint)))
			{
				if (o.Visible)
				{
					o.ComputeCuttingParametres(model.SelectedObject, this.currentCutViewpoint);
					count++;
				}
			}
			this.numberOfObjectsWithCoumputedCuttingParams = count;
		}

		private Camera findBestViewpointFor(GeometricObject focusedObject)
		{
			if (model.ViewpointSet.Count == 0)
				return this.myCamera;
			List<int> depths = new List<int>();
			for (int i = 0; i < model.ViewpointSet.Count; i++)
				depths.Add(model.ViewpointSet.GetOcclusionGraphAt(i).GetDepthOf(focusedObject) ?? int.MaxValue);
			List<int> bestViewpointsIndeces = new List<int>() { 0 };
			for (int i = 1; i < depths.Count; i++) // vyber viewpointy u nichz je hloubka nejnizsi
			{
				if (depths[i] < depths[bestViewpointsIndeces[0]])
				{
					bestViewpointsIndeces.Clear();
					bestViewpointsIndeces.Add(i);
				}
				else if(depths[i] == depths[bestViewpointsIndeces[0]])
					bestViewpointsIndeces.Add(i);
			}

			// vyber nejblizsi viewpoint k danemu objektu s viewpoint s nejmensim poctem rezani
			int bestIndex = bestViewpointsIndeces[0];
			Vector centerOfObject = focusedObject.AxesBoundingBox.Center;
			for (int i = 1; i < bestViewpointsIndeces.Count; i++)
			{
				Vector viepointPositionBest = model.ViewpointSet[bestIndex].Eye;
				Vector viewpointPositionActual = model.ViewpointSet[bestViewpointsIndeces[i]].Eye;

				if ((centerOfObject - viewpointPositionActual).GetLength() < (centerOfObject - viepointPositionBest).GetLength())
					bestIndex = bestViewpointsIndeces[i];
			}

			return model.ViewpointSet[bestIndex];
		}
	}
}
